/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package shaman.storage;

import java.io.IOException;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import shaman.storage.api.Filter;
import shaman.storage.api.Storage;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import com.thoughtworks.xstream.annotations.XStreamAsAttribute;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TreeMap;
import java.util.TreeSet;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JProgressBar;
import javax.swing.JTextArea;
import shaman.dialog.selectfolder.FolderSelect;
import shaman.storage.api.Backup;
import shaman.storage.common.FileProperties;

/**
 *
 * @author Shaman
 */
@XStreamAlias("task")
public class BackupTask{

    static final String PFN_DATE = "date";
    static final String PFN_FILE_SIZE = "size";
    static final String PFN_HASH = "hash";

    @XStreamAlias("rules")
    ArrayList<Rule> m_Rules = new ArrayList<Rule> ();
    @XStreamAlias("filters")
    ArrayList<Filter> m_Filters = new ArrayList<Filter>();
    @XStreamAlias("storages")
    ArrayList<Storage> m_Storages = new ArrayList<Storage>();
    @XStreamAsAttribute
    String Name = "Task name";
    
    private transient boolean hasConfigChanges = false;

    private transient TableModelRules tableModelRules = null;
    private transient TableModelFilters tableModelFilters = null;
    private transient TableModelStorages tableModelStorages = null;

    private transient boolean changed = false;

    public BackupTask()
    {
    }

    @Override
    public String toString()
    {
        return this.getName();
    }

    public boolean isChanged()
    {
        return changed;
    }

    public void setChanged(boolean ch)
    {
        changed = ch;
    }

    public String getName() {
        return Name;
    }

    public void setName(String Name) {
        this.Name = Name;
        hasConfigChanges = true;
    }

    public List<Rule> getRules()
    {
        return m_Rules;
    }

    public List<Filter> getFilters()
    {
        return m_Filters;
    }

    public List<Storage> getStorages()
    {
        return m_Storages;
    }

    public boolean hasConfigChanges() {
        return hasConfigChanges;
    }

    public void setConfigChanges(boolean hasConfigChanges) {
        this.hasConfigChanges = hasConfigChanges;
    }

    public TableModelRules getTableModelRules()
    {
        if (tableModelRules == null)
            tableModelRules = new TableModelRules(this);
        return tableModelRules;
    }

    public TableModelFilters getTableModelFilters()
    {
        if (tableModelFilters == null)
            tableModelFilters = new TableModelFilters(this);
        return tableModelFilters;
    }

    public TableModelStorages getTableModelStorages()
    {
        if (tableModelStorages == null)
            tableModelStorages = new TableModelStorages(this);
        return tableModelStorages;
    }

    public List<String> getFileList() throws IOException
    {
        if (m_Rules.isEmpty())
            throw new UnsupportedOperationException("Список правил для построения списка файлов не может быть пустым.\n Необходимо ввести хотя бы одно правило.");
        ArrayList<String> fileList = new ArrayList<String>();
        Iterator<Rule> RuleIterator = m_Rules.iterator();
        while (RuleIterator != null && RuleIterator.hasNext())
        {
            Rule rule = RuleIterator.next();
            if (rule.isAddable())
            {
                fileList.addAll(rule.getFileList());
            }
            else
            {
                List<String> rmFileNames = rule.getFileList();
                Iterator<String> rmFileNamesIterator = rmFileNames.iterator();
                while(rmFileNamesIterator.hasNext())
                {
                    String rmFileName = rmFileNamesIterator.next();
                    if (rmFileNames.contains(rmFileName))
                        rmFileNames.remove(rmFileName);
                }
            }
        }
        //FIXME: DEBUG CODE
        System.out.println("File list for archivation:");
        for (String fn: fileList)
            System.out.println(fn);
        //-----------------
        return fileList;
    }

    private Backup getLastBackup() throws IOException
    {
        Backup lastBackup = null;
        for (Storage st : m_Storages)
            if (st.isAccessible())
                for (Backup bk : st.getBackups())
                    if (lastBackup != null || bk.getBackupDate().compareTo(lastBackup.getBackupDate()) > 0)
                        lastBackup = bk;
        return lastBackup;
    }

    private TreeSet<String> getBackupFiles(Backup bk) throws IOException
    {
        TreeSet<String> backupFiles = new TreeSet<String>();
        if (bk != null)
        {
            List<String> lastBackupFiles = new ArrayList<String>();
            for (String s : bk.getFileList())
                backupFiles.add(s);
        }
        return backupFiles;
    }

    private DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss");

    private TreeSet<String> getChangedFiles(TreeSet<String> currentFiles) throws IOException, ParseException
    {
        //Выбор самого "свежего" бэкапа из хранилищ
        Backup lastBackup = getLastBackup();
        //Получаем список файлов в бэкапе
        TreeSet<String> backupFiles = getBackupFiles(lastBackup);
        //Создаём список в который будем вносить изменившиеся файлы
        TreeSet<String> changedFiles = new TreeSet<String>();
        for (String cf : currentFiles)
            if (backupFiles.contains(cf))
            {
                File fcf = new File(cf);
                if (dateFormat.parse(lastBackup.getFileProperties(cf).getProperty(FileProperties.PROPERTY_DATE)).getTime() < fcf.lastModified() ||
                    Long.valueOf(lastBackup.getFileProperties(cf).getProperty(FileProperties.PROPERTY_LENGTH)) == fcf.length())
                    changedFiles.add(cf);
            }
            else
            {
                changedFiles.add(cf);
            }
        return changedFiles;
    }

    private void createBackup(TreeSet<String> changedFiles, 
                                JFrame parent, 
                                JProgressBar progressBar,
                                JTextArea textLog)
        throws IOException
    {
        for (Storage st : m_Storages)
        {
            Backup bk = st.newBackup(m_Filters);
            System.out.println("Storage: " + st.getLocation());
            for (String pathName : changedFiles)
            {
                File f = new File(pathName);
                Map<String, String> properties = new TreeMap<String, String>();
                properties.put(PFN_FILE_SIZE, String.valueOf(f.length()));
                properties.put(PFN_DATE, dateFormat.format(f.lastModified()));

                //Добавляем содержимое файла в бэкап
                try {
                    addFileToBackup(f, bk, properties);
                } catch (IOException exc) {
                    textLog.append("\t" + pathName + " (date = " + properties.get(PFN_DATE) + ") Ошибка ввода\\вывода " + exc.getLocalizedMessage() + "\n");
                } finally {
                    textLog.append("\t" + pathName + " (date = " + properties.get(PFN_DATE) + ") добавлен в хранилище;\n");
                }
            }
            try {
                bk.finish();
            } catch (IOException ex) {
                Logger.getLogger(BackupTask.class.getName()).log(Level.SEVERE, null, ex);
                textLog.append("Ошибка завершения бекапа");
                textLog.append("Исключение: " + ex.getLocalizedMessage() + "\n");
            }
        }
    }


    public boolean run(JFrame parent, JProgressBar progressBar, JTextArea textLog)
            throws IOException, ParseException
    {
        
        if (m_Storages.isEmpty())
            throw new UnsupportedOperationException("Список хранилищ не может быть пустым. Необходимо внести в список хотя бы одно хранилище.");
        //Построение текущего списка файлов
        TreeSet<String> curFiles = new TreeSet(getFileList());
        if (curFiles.isEmpty())
            throw new UnsupportedOperationException("Данному списку правил, для построения списка файлов, не соответсвует ни один файл.");
        //Определяем различия в списках файлов
        TreeSet<String> changedFiles = getChangedFiles(curFiles);
        //Содаём бэкап
        createBackup(changedFiles, parent, progressBar, textLog);
        return true;
    }

    public void restore(JFrame parent, JProgressBar progressBar, JTextArea textLog)
    {
        //progressBar.setMinimum(0);
        //progressBar.setValue(0);
        //Получаем список хранилищ в задании
        List<Storage> storageList = this.getStorages();
        if (storageList == null || storageList.size() < 1)
        {
            JOptionPane.showMessageDialog(parent, "Список хранилищ пуст!", "ERROR", JOptionPane.ERROR_MESSAGE);
            return;
        }
        //Определяем последний бэкап
        Backup lastBackup = null;
        Date lastBackupDate = null;
        for (Storage storage: storageList)
        {
            //Получаем список бэкапов в хранилище
            List<Backup> backupList = null;
            try {
                backupList = storage.getBackups();
            } catch (Exception exc) {
                textLog.append("RESTORE ERROR: IOException в Storage.getBackups();\n");
                textLog.append("RESTORE ERROR: Exception: "+exc.getLocalizedMessage()+"\n");
                continue;
            }
            //Проверяем все бэкапы и находим самый свежий
            for (Backup backup: backupList)
            {
                //Получаем дату текущего бэкапа
                Date backupDate = null;
                try {
                    backupDate = backup.getBackupDate();
                } catch (Exception exc) {
                    //В случае ошибки выводим информацию и переходим к следующему бэкапу
                    textLog.append("RESTORE ERROR: IOException в choosing last backup.\n");
                    textLog.append("RESTORE ERROR: Exception: "+exc.getLocalizedMessage()+"\n");
                    continue;
                }
                if (lastBackupDate == null || lastBackupDate.before(backupDate))
                {
                    lastBackup = backup;
                    lastBackupDate = backupDate;
                }
            }
        }
        if (lastBackup == null)
        {
            JOptionPane.showMessageDialog(parent, "Из списка хранилищ неудалось получить ни одного бэкапа.", "Ошибка", JOptionPane.INFORMATION_MESSAGE);
            textLog.append("ERROR: Из списка хранилищ неудалось получить ни одного бэкапа.\n");
            return;
        }
        //Восстанавливаем файлы
        String restoreDirectory = FolderSelect.show(parent, "Выберите папку для восстановления файлов.");
        Iterable<String> fileListIterator = null;
        try {
            fileListIterator = lastBackup.getFileList();
        } catch (IOException exc) {
            textLog.append("RESTORE ERROR: IOException при получении списка файлов из бэкапа\n");
            textLog.append("Exception: "+exc.getLocalizedMessage()+"\n");
        }
        for (String backupFileName: fileListIterator)
        {
            //Получаем имя файла в который будет производиться восстановление.
            StringBuilder backupFileNameBuilder = new StringBuilder(backupFileName);
            backupFileNameBuilder.deleteCharAt(2);
            String restoredFileName = restoreDirectory + backupFileNameBuilder.toString();
            //Получаем поток в который восстановим файл
            FileOutputStream restoredFileStream = null;
            try {
                restoredFileStream = new FileOutputStream(restoredFileName);
            } catch (Exception exc) {
                //В случае ошибки переходим к следующему файлу.
                textLog.append("RESTORED FILE ERROR: Ошибка создания файла при восстановлении.\n");
                textLog.append("Exception: " + exc.getLocalizedMessage()+"\n");
                continue;
            }
            //Получаем поток файла в бэкапе
            InputStream backupFileStream = null;
            try
            {
                backupFileStream = lastBackup.getFileData(backupFileName);
            } catch (Exception exc) {
                //В случае ошибки переходим к следующему файлу.
                textLog.append("RESTORED FILE ERROR: Ошибка получения данных файла из хранилища.\n");
                textLog.append("Exception: " + exc.getLocalizedMessage()+"\n");
                continue;
            }
            //Восстанавливаем содержимое файла
            byte[] bytes = new byte[32768];
            try {
                while (backupFileStream.read(bytes) > 0)
                    restoredFileStream.write(bytes);
            } catch (Exception exc) {
                //В случае ошибки переходим к следующему файлу.
                textLog.append("RESTORED FILE ERROR: Ошибка записи данных в файл при восстановлении.\n");
                textLog.append("Exception: " + exc.getLocalizedMessage());
            } finally {
                try {
                    restoredFileStream.close();
                } catch (Exception ex) {
                    textLog.append(ex.getLocalizedMessage());
                }
            }
        }
    }

    private void addFileToBackup(File f, Backup bk, Map<String, String> properties)
            throws IOException {
        OutputStream backupFileStream = bk.createFile(f.getAbsolutePath(), properties);
        InputStream currentFileStream = new FileInputStream(f);
        byte[] currentFileBytes = new byte[32768];
        while (currentFileStream.read(currentFileBytes) > 0)
            backupFileStream.write(currentFileBytes);
        backupFileStream.close();
        currentFileStream.close();
    }
}
